%% Message passing utility.
%% User interface:
%% logon(Name)
%%      One user at a time can log in from each Erlang node in the
%%      system messenger and choose a suitable Name. If the Name 
%%      is already logged in at another node or if someone else is 
%%      already logged in at the same node, login will be rejected 
%%      with a suitable error message.
%% logoff()
%%      Logs off anybody at any node.
%% message(ToName, Message)
%%      Sends Message to ToName. Error messages if the user of this
%%      function is not logged on or if ToName is not logged on at
%%      any node.
%% 
%% One node in the network of Erlang nodes runs a server which maintians
%% data about the logged on users. The server is registered as "messenger".
%% Each node where there is a user logged on runs a client process
%% registered as "mess_client".
%%
%% Protocol between the client processes and the server
%% ----------------------------------------------------
%%
%% To server: {ClientPid, logon, UserName}
%% Reply {messenger, stop, user_exists_at_other_node} stops the client
%% Reply {messenger, logged_on} logon was successful
%%
%% To server: {ClientPid, logoff}
%% Reply: no reply
%%
%% To server: {ClientPid, message_to, ToName, Message} send a message
%% Reply: {messenger, stop, you_are_not_logged_on} stops the client
%% Reply: {messenger, receiver_not_found} no user with this name logged on
%% Reply: {messenger, sent} Message has been sent (but no guarantee)
%% 
%%
%% To client: {message_from, Name, Message}
%% 
%% Protocol between the "commands" and the client
%% ----------------------------------------------
%%
%% Started: messenger:client(Server_Node, Name)
%% To client: logoff
%% To client: {message_to, ToName, Message}
%%
%% Configuration: change the serv_node() function to return the 
%% name of the node where the messenger server runs

-module(messenger).
-export([start_server/0, server/1, logon/1, logoff/0, message/2, client/2]).

%% Change the function below to return the name of the node where the 
%% messenger server runs
server_node() ->
    messenger@Xunix.

%% This is the server process for the "messenger"
%% the user list has the format [{ClientPid1, Name1}, {ClientPid2,Name2}, ...]
server() ->
    process_flag(trap_exit, true),
    server([]).

server(User_List) ->
    receive
        {From, logon, Name} ->
            New_User_List = server_logon(From, Name, User_List),
            server(New_User_List);
        {'EXIT', From, _} ->
            New_User_List = server_logoff(From, User_List),
            server(New_User_List);
        {From, message_to, To, Message}  ->
            server_transfer(From, To, Message, User_List),
            io:format("list is now: ~p~n", [User_List]),
            server(User_List)
    end.

%% Start the server
start_server() ->
    register(messenger, spawn(messenger, server, [])).

%% Server adds a new user to the user list
server_logon(From, Name, User_List) ->
    %% check if logged on anywhere else
    case lists:keymember(Name, 2, User_List) of 
        true ->
            From ! {messenger, stop, user_exists_at_other_node},
            User_List;
        false ->
            From ! {messenger, logged_on},
            link(From),
            [{From, Name}|User_List]
    end.

%% Server deletes a user from the user list
server_logoff(From, User_List) ->
    lists:keydelete(From, 1, User_List).

%% Server transfers a message between user
server_transfer(From, To, Message, User_List) ->
    %% check that the user is logged on and who he is
    case lists:keysearch(From, 1, User_List) of
        false ->
            From ! {messenger, stop, you_are_not_logged_on};
        {value, {From, Name}} ->
            server_transfer(From, Name, To, Message, User_List)
    end.

%% If the user exists, send the message
server_transfer(From, Name, To, Message, User_List) ->
    %% Find the receiver and send the message
    case lists:keysearch(To, 2, User_List) of
        false ->
            From ! {messenger, receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! {message_from, Name, Message},
            From ! {messenger, sent}
    end.

%% User Commands
logon(Name) ->
    case whereis(mess_client) of
        undefined ->
            register(mess_client, 
                spawn(messenger, client, [server_node(), Name]));
        _ -> already_logged_on
    end.

logoff() ->
    mess_client ! logoff.

message(ToName, Message) ->
    case whereis(mess_client) of
        undefined ->
            not_logged_on;
        _ -> mess_client ! {message_to, ToName, Message},
        ok
    end.

%% The client process which runs on each node
client(ServerNode, Name) ->
    {messenger, ServerNode} ! {self(), logon, Name},
    await_result(),
    client(ServerNode).

client(ServerNode) ->
    receive
        logoff ->
            exit(normal);
        {message_to, ToName, Message} ->
            {messenger, ServerNode} ! {self(), message_to, ToName, Message},
            await_result();
        {message_from, From, Message} ->
            io:format("Message from ~p: ~p~n", [From, Message])
    end,
    client(ServerNode).

%% wait for a response from the server
await_result() ->
    receive
        {messenger, stop, Why} ->
            io:format("~p~n", [Why]),
            exit(normal);
        {messenger,What} ->
            io:format("~p~n", [What])
    after 5000 ->    % five second
        io:format("No response from server~n", []),
        exit(timeout).
    end.
